﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.Xml.Serialization;
using Elca.MvvmHelpers;

namespace ULSDeobfuscator {
    public class MainWindowViewModel : BaseViewModel, IDropTarget, IColumnCommunication {

        private const string Title = "ULS Deobfuscator";

        public string HighlightRulePrefix => "Highlight ";

        private const double DoubleTolerance = 0.1;

        private static readonly Brush s_annotationBrush = Brushes.Blue;
        private static readonly Brush s_searchBrush = Brushes.Orange;

        //private static readonly Color s_annotationLightColor = Color.FromArgb(0xFF, 0x22, 0x7C, 0x1F);
        //private static readonly Color s_annotationDarkColor  = Color.FromArgb(0xFF, 0x0E, 0x45, 0x0C);

        //private static readonly Color s_searchLightColor = Color.FromArgb(0xFF, 0x1F, 0x4E, 0x7C);
        //private static readonly Color s_searchDarkColor  = Color.FromArgb(0xFF, 0x0C, 0x30, 0x45);

        private readonly LogReader _lr = new LogReader();
        private List<LogFileInfo> _filesToRead;
        private readonly List<LogFileInfo> _filesInMemory = new List<LogFileInfo>();

        private readonly ObservableCollection<LogEntry> _allLogEntries = new ObservableCollection<LogEntry>();
        private ObservableCollection<LogEntry> _filteredLogEntries = new ObservableCollection<LogEntry>();
        private readonly object _filteredLogLock = new object();

        private string _status;

        private DateTime _minAvailableDate, _maxAvailableDate;
        private DateTime _minChosenDate;
        private DateTime _maxChosenDate;

        private bool _userChoseToReplaceLogFile;
        private DateTime _userChoseToGoAtThisTime;

        /// <summary>
        ///  number of bytes already read in the last files. If it changes, reading will start at this offset
        /// </summary>
        private long _lastFileReadOffset;
        private FileSystemWatcher _lastFileWatcher;
        private FileSystemWatcher _newFileWatcher;

        private readonly Dispatcher _uiDispatcher;

        public ICommand CloseCommand { get; private set; }
        public ICommand LoadCommand { get; private set; }
        public ICommand OpenCommand { get; private set; }
        public ICommand SaveCommand { get; private set; }
        public ICommand CopyDateCommand { get; private set; }

        public ICommand CopyThreadIdCommand { get; private set; }

        public ICommand MessageFilterCommand { get; private set; }
        public ICommand ClearMessageFilterCommand { get; private set; }
        
        public ICommand CorrelationFilterCommand { get; private set; }
        public ICommand ClearCorrelationFilterCommand { get; private set; }

        public ICommand IncludeCorrelationCommand { get; private set; }
        public ICommand HighlightCorrelationCommand { get; private set; }
        public ICommand CopyCorrelationCommand { get; private set; }
        public ICommand FocusToMessageSearchBoxCommand { get; private set; }

        public ICommand ManageComplexFiltersCommand { get; private set; }

        public ICommand JumpToEntryCommand { get; private set; }

        public ICommand AboutCommand { get; private set; }

        public ICommand MessageSearchCommand { get; private set; }
        public ICommand MessageSearchPreviousCommand { get; private set; }
        public ICommand MessageSearchNextCommand { get; private set; }
        public ICommand DeleteAllSearchMarksCommand { get; private set; }

        public ICommand DisplayTimeStartPopupCommand { get; private set; }
        public ICommand DisplayShowColumnsPopupCommand { get; private set; }
        public ICommand DisplayShowOthersPopupCommand { get; private set; }

        public ICommand ManageColorsCommand { get; private set; }

        public ICommand ApplyTimelineSelectionFilterCommand { get; private set; }
        public ICommand ApplyTimeIntervalCommand { get; private set; }
        public ICommand ShowFullTimeIntervalCommand { get; private set; }

        public ICommand CopyTextCommand { get; private set; }
        public ICommand SearchTextCommand { get; private set; }
        public ICommand CopyLineCommand { get; private set; }
        public ICommand CopyMessageCommand { get; private set; }

        public ICommand ExcludeLinesBeforeCommand { get; private set; }
        public ICommand ExcludeLinesAfterCommand { get; private set; }

        public ICommand DropReplaceFilesCommand { get; private set; }
        public ICommand DropAddFilesCommand { get; private set; }
        public ICommand CancelOperationCommand { get; private set; }

        public string WindowTitle => $"{Title} version {App.VersionNumber}";

        public ObservableCollection<LogEntry> DisplayedLogEntries {
            get {
                return _filteredLogEntries;
            }
        }

        public string Status {
            get { return _status; }
            set {

                if (_status != value) {
                    _status = value;
                    OnPropertyChanged("Status");
                }
            }
        }

        public string LineInformation {
            get {
                // let's assume that there is more than 1 line
                return String.Format("Showing {0:N0} of {1:N0} lines", _filteredLogEntries.Count, _allLogEntries.Count);
            }
        }

        public string DisplayedMinAvailableLogDate {
            get { return _minAvailableDate.ToDisplayString(); }
        }

        public DateTime MinChosenLogDate {
            get { return _minChosenDate; }
            set {
                if (!_minChosenDate.Equals(value)) {
                    _minChosenDate = value;

                    OnPropertyChanged("MinChosenLogDate");
                }
            }
        }

        public DateTime MaxChosenLogDate {
            get { return _maxChosenDate; }
            set {
                if (!_maxChosenDate.Equals(value)) {
                    _maxChosenDate = value;

                    OnPropertyChanged("MaxChosenLogDate");
                }
            }
        }

        public string DisplayedMaxAvailableLogDate {
            get { return _maxAvailableDate.ToDisplayString(); }
        }

        private string _chosenMiddleLogTime;
        public string ChosenMiddleLogTime {
            get { return _chosenMiddleLogTime; }
            set {
                if (_chosenMiddleLogTime != value) {
                    _chosenMiddleLogTime = value;
                    OnPropertyChanged("ChosenMiddleLogTime");
                }
            }
        }

        private string _aroundTimeQuantity;
        public string AroundTimeQuantity {
            get { return _aroundTimeQuantity; }
            set {
                if (_aroundTimeQuantity != value) {
                    _aroundTimeQuantity = value;
                    OnPropertyChanged("AroundTimeQuantity");
                }
            }
        }

        private const int SecondUnit = 0;
        private const int MinuteUnit = 1;
        private const int HourUnit = 2;

        private int _aroundTimeUnit = MinuteUnit;
        public int AroundTimeUnit {
            get { return _aroundTimeUnit; }
            set {
                if (_aroundTimeUnit != value) {
                    _aroundTimeUnit = value;
                    OnPropertyChanged("AroundTimeUnit");
                }
            }
        }

        public GridLength DetailBarHeight {
            get { return new GridLength(App.GlobalSettings.MainTextBoxHeight); }
            set {
                double height = value.Value;
                if (Math.Abs(height - App.GlobalSettings.MainTextBoxHeight) > DoubleTolerance) {
                    App.GlobalSettings.MainTextBoxHeight = height;
                    OnPropertyChanged("DetailBarHeight");
                }
            }
        }

        public double WindowHeight {
            get { return App.GlobalSettings.MainWindowHeight; }
            set {
                if (Math.Abs(App.GlobalSettings.MainWindowHeight - value) > DoubleTolerance) {
                    App.GlobalSettings.MainWindowHeight = value;
                    OnPropertyChanged("WindowHeight");
                }
            }
        }

        public double WindowWidth {
            get { return App.GlobalSettings.MainWindowWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainWindowWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainWindowWidth = value;
                    OnPropertyChanged("WindowWidth");
                }
            }
        }

        public double WindowLeft {
            get { return App.GlobalSettings.MainWindowLeft; }
            set {
                if (Math.Abs(App.GlobalSettings.MainWindowLeft - value) > DoubleTolerance) {
                    App.GlobalSettings.MainWindowLeft = value;
                    OnPropertyChanged("WindowLeft");
                }
            }
        }

        public double WindowTop {
            get { return App.GlobalSettings.MainWindowTop; }
            set {
                if (Math.Abs(App.GlobalSettings.MainWindowTop - value) > DoubleTolerance) {
                    App.GlobalSettings.MainWindowTop = value;
                    OnPropertyChanged("WindowTop");
                }
            }
        }

        public WindowState WindowState {
            get { return App.GlobalSettings.MainWindowState; }
            set {
                if (App.GlobalSettings.MainWindowState != value) {
                    App.GlobalSettings.MainWindowState = value;
                    OnPropertyChanged("WindowState");
                }
            }
        }

        public bool IsDateShown {
            get { return App.GlobalSettings.IsDateShown; }
            set {
                if (App.GlobalSettings.IsDateShown != value) {
                    App.GlobalSettings.IsDateShown = value;
                    OnPropertyChanged("IsDateShown");
                    OnPropertyChanged("DateColumnWidth");
                }
            }
        }

        public double DateColumnWidth {
            get { return App.GlobalSettings.MainDateWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainDateWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainDateWidth = value;
                    OnPropertyChanged("DateColumnWidth");

                    // columns cannot be really hidden in a ListView; their width can be set to 0
                    // but the user is able to show them again with the mouse.
                    // When that happens, we need to synchronize our boolean visible property
                    if (DateColumnWidth > 0) {
                        IsDateShown = true;
                    }
                }
            }
        }

        public bool IsProcessShown {
            get { return App.GlobalSettings.IsProcessShown; }
            set {
                if (App.GlobalSettings.IsProcessShown != value) {
                    App.GlobalSettings.IsProcessShown = value;
                    OnPropertyChanged("IsProcessShown");
                }
            }
        }

        public double ProcessColumnWidth {
            get { return App.GlobalSettings.MainProcessWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainProcessWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainProcessWidth = value;
                    OnPropertyChanged("ProcessColumnWidth");
                    if (ProcessColumnWidth > 0) {
                        IsProcessShown = true;
                    }
                }
            }
        }

        public bool IsThreadIdShown {
            get { return App.GlobalSettings.IsThreadIdShown; }
            set {
                if (App.GlobalSettings.IsThreadIdShown != value) {
                    App.GlobalSettings.IsThreadIdShown = value;
                    OnPropertyChanged("IsThreadIdShown");
                }
            }
        }

        public double ThreadIdColumnWidth {
            get { return App.GlobalSettings.MainThreadIdWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainThreadIdWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainThreadIdWidth = value;
                    OnPropertyChanged("ThreadIdColumnWidth");
                    if (ThreadIdColumnWidth > 0) {
                        IsThreadIdShown = true;
                    }
                }
            }
        }

        public bool IsAreaShown {
            get { return App.GlobalSettings.IsAreaShown; }
            set {
                if (App.GlobalSettings.IsAreaShown != value) {
                    App.GlobalSettings.IsAreaShown = value;
                    OnPropertyChanged("IsAreaShown");
                }
            }
        }

        public double AreaColumnWidth {
            get { return App.GlobalSettings.MainAreaWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainAreaWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainAreaWidth = value;
                    OnPropertyChanged("AreaColumnWidth");
                    if (AreaColumnWidth > 0) {
                        IsAreaShown = true;
                    }
                }
            }
        }

        public bool IsCategoryShown {
            get { return App.GlobalSettings.IsCategoryShown; }
            set {
                if (App.GlobalSettings.IsCategoryShown != value) {
                    App.GlobalSettings.IsCategoryShown = value;
                    OnPropertyChanged("IsCategoryShown");
                }
            }
        }

        public double CategoryColumnWidth {
            get { return App.GlobalSettings.MainCategoryWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainCategoryWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainCategoryWidth = value;
                    OnPropertyChanged("CategoryColumnWidth");
                    if (CategoryColumnWidth > 0) {
                        IsCategoryShown = true;
                    }
                }
            }
        }

        public bool IsEventIdShown {
            get { return App.GlobalSettings.IsEventIdShown; }
            set {
                if (App.GlobalSettings.IsEventIdShown != value) {
                    App.GlobalSettings.IsEventIdShown = value;
                    OnPropertyChanged("IsEventIdShown");
                }
            }
        }

        public double EventIdColumnWidth {
            get { return App.GlobalSettings.MainEventIdWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainEventIdWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainEventIdWidth = value;
                    OnPropertyChanged("EventIdColumnWidth");
                    if (EventIdColumnWidth > 0) {
                        IsEventIdShown = true;
                    }
                }
            }
        }

        public bool IsLevelShown {
            get { return App.GlobalSettings.IsLevelShown; }
            set {
                if (App.GlobalSettings.IsLevelShown != value) {
                    App.GlobalSettings.IsLevelShown = value;
                    OnPropertyChanged("IsLevelShown");
                }
            }
        }

        public double LevelColumnWidth {
            get { return App.GlobalSettings.MainLevelWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainLevelWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainLevelWidth = value;
                    OnPropertyChanged("LevelColumnWidth");
                    if (LevelColumnWidth > 0) {
                        IsLevelShown = true;
                    }
                }
            }
        }

        public bool IsMessageShown {
            get { return App.GlobalSettings.IsMessageShown; }
            set {
                if (App.GlobalSettings.IsMessageShown != value) {
                    App.GlobalSettings.IsMessageShown = value;
                    OnPropertyChanged("IsMessageShown");
                }
            }
        }

        public double MessageColumnWidth {
            get { return App.GlobalSettings.MainMessageWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainMessageWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainMessageWidth = value;
                    OnPropertyChanged("MessageColumnWidth");
                    if (MessageColumnWidth > 0) {
                        IsMessageShown = true;
                    }
                }
            }
        }

        public bool IsCorrelationShown {
            get { return App.GlobalSettings.IsCorrelationShown; }
            set {
                if (App.GlobalSettings.IsCorrelationShown != value) {
                    App.GlobalSettings.IsCorrelationShown = value;
                    OnPropertyChanged("IsCorrelationShown");
                }
            }
        }

        public double CorrelationColumnWidth {
            get { return App.GlobalSettings.MainCorrelationWidth; }
            set {
                if (Math.Abs(App.GlobalSettings.MainCorrelationWidth - value) > DoubleTolerance) {
                    App.GlobalSettings.MainCorrelationWidth = value;
                    OnPropertyChanged("CorrelationColumnWidth");
                    if (CorrelationColumnWidth > 0) {
                        IsCorrelationShown = true;
                    }
                }
            }
        }

        private int _selectedLogEntryIndex;
        public int SelectedLogEntryIndex {
            get { return _selectedLogEntryIndex; }
            set {
                if (_selectedLogEntryIndex != value) {
                    _selectedLogEntryIndex = value;
                    OnPropertyChanged("SelectedLogEntryIndex");
                }
            }
        }

        private LogEntry _selectedLogEntry;

        public LogEntry SelectedLogEntry {
            get { return _selectedLogEntry; }
            set {
                if (_selectedLogEntry != value) {
                    _selectedLogEntry = value;
                    OnPropertyChanged("SelectedLogEntry");
                }
            }
        }

        private List<LogEntry> _selectedLogEntries;
        public List<LogEntry> SelectedLogEntries {
            get { return _selectedLogEntries; }
            set {
                if (_selectedLogEntries != value) {
                    _selectedLogEntries = value;
                    OnPropertyChanged("SelectedLogEntries");
                }
            }
        }

        public CommandBindingCollection LogCommandBindings { get; private set; }

        private readonly AbstractLogColumnViewModel[] _columnsInfo;

        public AbstractLogColumnViewModel ProcessColumn {
            get { return _columnsInfo[0]; }
            private set { _columnsInfo[0] = value; }
        }

        public AbstractLogColumnViewModel AreaColumn {
            get { return _columnsInfo[1]; }
            private set { _columnsInfo[1] = value; }
        }

        public AbstractLogColumnViewModel CategoryColumn {
            get { return _columnsInfo[2]; }
            private set { _columnsInfo[2] = value; }
        }

        public AbstractLogColumnViewModel LevelColumn {
            get { return _columnsInfo[3]; }
            private set { _columnsInfo[3] = value; }
        }

        public AbstractLogColumnViewModel EventColumn {
            get { return _columnsInfo[4]; }
            private set { _columnsInfo[4] = value; }
        }


        public ObservableCollection<string> RecentSearchMessages { get; private set; }
        public ObservableCollection<string> RecentSearchFilters { get; private set; }

        // do not check for message emptiness at each log entry
        private bool _isMessageFilterNonEmpty; // false by default => do not filter on message;
        private string _messageFilter;
        private string _lowerCaseMessageFilter;

        public string MessageFilter {
            get { return _messageFilter; }
            set {
                if (_messageFilter != value) {
                    _messageFilter = value;
                    _isMessageFilterNonEmpty = !String.IsNullOrEmpty(_messageFilter);
                    _lowerCaseMessageFilter = _isMessageFilterNonEmpty
// ReSharper disable PossibleNullReferenceException
                                                      ? _messageFilter.Trim().ToLowerInvariant()
// ReSharper restore PossibleNullReferenceException
                                                      : String.Empty;

                    OnPropertyChanged("MessageFilter");
                }
            }
        }

        private bool _isMessageFilterRegex;
        public bool IsMessageFilterRegex {
            get { return _isMessageFilterRegex; }
            set {
                if (_isMessageFilterRegex != value) {
                    _isMessageFilterRegex = value;
                    OnPropertyChanged("IsMessageFilterRegex");
                }
            }
        }

        // do not check for correlation emptiness at each log entry
        private bool _isCorrelationFilterNonEmpty; // false by default => do not filter on correlation;
        private string _correlationFilter;
        private string _lowerCaseCorrelationFilter;

        public string CorrelationFilter {
            get { return _correlationFilter; }
            set {
                if (_correlationFilter != value) {
                    _correlationFilter = value;
                    _isCorrelationFilterNonEmpty = !String.IsNullOrEmpty(_correlationFilter);
                    _lowerCaseCorrelationFilter = _isCorrelationFilterNonEmpty
// ReSharper disable PossibleNullReferenceException
                                                          ? _correlationFilter.Trim().ToLowerInvariant()
// ReSharper restore PossibleNullReferenceException
                                                          : String.Empty;
                    OnPropertyChanged("CorrelationFilter");
                }
            }
        }

        private string _displayedFilesList;

        public string DisplayedFilesList {
            get { return _displayedFilesList; }
            set {
                if (_displayedFilesList != value) {
                    _displayedFilesList = value;
                    OnPropertyChanged("DisplayedFilesList");
                }
            }
        }

        private bool _isMessageSearchBoxFocused;

        public bool IsMessageSearchBoxFocused {
            get { return _isMessageSearchBoxFocused; }
            set {
                if (_isMessageSearchBoxFocused != value) {
                    _isMessageSearchBoxFocused = value;
                    OnPropertyChanged("IsMessageSearchBoxFocused");
                }
            }
        }


        private LogEntry _logEntryToShow;
        public LogEntry LogEntryToShow {
            get { return _logEntryToShow; }
            set {
                if (_logEntryToShow != value) {
                    _logEntryToShow = value;
                    OnPropertyChanged("LogEntryToShow");
                }
            }
        }

        private string _manageComplexFilterCaption;
        public string ManageComplexFilterCaption {
            get { return _manageComplexFilterCaption; }
            set {
                if (_manageComplexFilterCaption != value) {
                    _manageComplexFilterCaption = value;
                    OnPropertyChanged("ManageComplexFilterCaption");
                }
            }
        }

        private double _timelineWidth;
        public double TimelineWidth {
            get { return _timelineWidth; }
            set {
                if (Math.Abs(_timelineWidth - value) > DoubleTolerance) {
                    _timelineWidth = value;
                    RecomputeToolbarWidths();
                    OnPropertyChanged("TimelineWidth");
                }
            }
        }

        public List<TimelineInfo> TimelineGroups { get; private set; }

        /// <summary>
        /// The width in ms of the timeline representation, i.e. the number of ms shown on the screen at a given time (but 0 if the timeline is not shown)
        /// </summary>
        private long _timelineGroupsWidth;
        public long TimelineGroupsWidth {
            get { return _timelineGroupsWidth; }
            set {
                if (_timelineGroupsWidth != value) {
                    _timelineGroupsWidth = value;
                    OnPropertyChanged("TimelineGroupsWidth");
                }
            }
        }


        private Visibility _timelineVisibility = Visibility.Collapsed;
        public Visibility TimelineVisibility {
            get { return _timelineVisibility; }
            set {
                if (_timelineVisibility != value) {
                    _timelineVisibility = value;
                    OnPropertyChanged("TimelineVisibility");
                }
            }
        }

        private bool _isTimelineDisplayActivated;
        public bool IsTimelineDisplayActivated {
            get { return _isTimelineDisplayActivated; }
            set {
                if (_isTimelineDisplayActivated != value) {
                    _isTimelineDisplayActivated = value;
                    OnPropertyChanged("IsTimelineDisplayActivated");
                }
            }
        }


        private bool _isTimelineToBeShown;
        public bool IsTimelineToBeShown {
            get { return _isTimelineToBeShown; }
            set {
                if (_isTimelineToBeShown != value) {
                    _isTimelineToBeShown = value;
                    OnPropertyChanged("IsTimelineToBeShown");

                    if (_isTimelineToBeShown) {
                        if (_filteredLogEntries.Count > 300 * 1000) {

                            IMessageToUser msg = ServiceRegistry.Resolve<IMessageToUser>();
                            if (msg.AskQuestion(Title, 
                                                "There are quite a few log entries. Displaying a time line will take a long time " +
                                                "and will consume a lot of resources\nAre you sure that you want to do this?", 
                                                QuestionType.YesNo) == AnswerType.No) {

                                _uiDispatcher.BeginInvoke(new Action(() => IsTimelineToBeShown = false));
                                return;
                            }
                        }

                        // if the timeline stuff has not been computed yet or has been reset
                        if (TimelineGroupsWidth == 0) {
                            ComputeTimelineGroups();
                        }
                    }
                    TimelineVisibility = _isTimelineToBeShown ? Visibility.Visible : Visibility.Collapsed;
                }
            }
        }

        private const int TimelineProcessIndex = 0;
        private const int TimelineThreadIDIndex = 1;
        private const int TimelineAreaIndex = 2;
        private const int TimelineCategoryIndex = 3;
        private const int TimelineLevelIndex = 4;

        private int _timelineSelectedIndex;
        public int TimelineSelectedIndex {
            get { return _timelineSelectedIndex; }
            set {
                if (_timelineSelectedIndex != value) {
                    _timelineSelectedIndex = value;
                    OnPropertyChanged("TimelineSelectedIndex");

                    RecomputeTimelineOnPropertyChanged();
                }
            }
        }

        private string _messageSearch;
        public string MessageSearch {
            get { return _messageSearch; }
            set {
                if (_messageSearch != value) {
                    _messageSearch = value;
                    OnPropertyChanged("MessageSearch");
                }
            }
        }

        private bool _isShowingTimeStartPopup;
        public bool IsShowingTimeStartPopup {
            get { return _isShowingTimeStartPopup; }
            set {
                if (_isShowingTimeStartPopup != value) {
                    _isShowingTimeStartPopup = value;
                    OnPropertyChanged("IsShowingTimeStartPopup");
                }
            }
        }

        private bool _isShowingShowColumnsPopup;
        public bool IsShowingShowColumnsPopup {
            get { return _isShowingShowColumnsPopup; }
            set {
                if (_isShowingShowColumnsPopup != value) {
                    _isShowingShowColumnsPopup = value;
                    OnPropertyChanged("IsShowingShowColumnsPopup");
                }
            }
        }

        private bool _isShowingOthersPopup;
        public bool IsShowingOthersPopup {
            get { return _isShowingOthersPopup; }
            set {
                if (_isShowingOthersPopup != value) {
                    _isShowingOthersPopup = value;
                    OnPropertyChanged("IsShowingOthersPopup");
                }
            }
        }

        private ObservableCollection<double> _toolbarColumnWidths;
        public ObservableCollection<double> ToolbarColumnWidths {
            get { return _toolbarColumnWidths; }
            set {
                if (_toolbarColumnWidths != value) {
                    _toolbarColumnWidths = value;
                    OnPropertyChanged("ToolbarColumnWidths");
                }
            }
        }

        private bool _isToolbarColumnWidthReady;
        public bool IsToolbarColumnWidthReady {
            get { return _isToolbarColumnWidthReady; }
            set {
                if (_isToolbarColumnWidthReady != value) {
                    _isToolbarColumnWidthReady = value;
                    OnPropertyChanged("IsToolbarColumnWidthReady");

                    if (_isToolbarColumnWidthReady) {
                        RememberToolbarWidths();
                        RecomputeToolbarWidths();
                    }
                }
            }
        }

        private bool _isAutoScrolling;
        public bool IsAutoScrolling {
            get { return _isAutoScrolling; }
            set {
                if (_isAutoScrolling != value) {
                    _isAutoScrolling = value;
                    OnPropertyChanged("IsAutoScrolling");
                }
            }
        }

        private bool _isAutoScrollingEnabled;
        public bool IsAutoScrollingEnabled {
            get { return _isAutoScrollingEnabled; }
            set {
                if (_isAutoScrollingEnabled != value) {
                    _isAutoScrollingEnabled = value;
                    OnPropertyChanged("IsAutoScrollingEnabled");
                }
            }
        }

        private bool _isAutoRefresh;
        public bool IsAutoRefresh {
            get { return _isAutoRefresh; }
            set {
                if (_isAutoRefresh != value) {
                    _isAutoRefresh = value;
                    OnPropertyChanged("IsAutoRefresh");
                }

                if (_isAutoRefresh) {
                    EnableFileWatching();
                    IsAutoScrollingEnabled = true;
                } else {
                    DisableFileWatching();
                    IsAutoScrollingEnabled = false;
                }
            }
        }

        /// <summary>
        /// Modified by the text box that holds the full message
        /// </summary>
        public string ParsedMessageSelectedText {
            get {
                return _parsedMessageSelectedText;
            }
            set {
                if (_parsedMessageSelectedText != value) {

                    _parsedMessageSelectedText = value;
                    OnPropertyChanged("ParsedMessageSelectedText");
                }
            }
        }

        public IGetSelection GetSelection { get; set; }

        public bool IsShowingDropDialog {
            get { return _isShowingDropDialog; }
            set {
                _isShowingDropDialog = value;
                OnPropertyChanged("IsShowingDropDialog");
            }
        }

        public int FilesDropCount {
            get { return _filesDropCount; }
            set {
                _filesDropCount = value;
                OnPropertyChanged("FilesDropCount");
            }
        }

        public SortingObservableCollection<AnnotationInfo, int> Annotations { get; }

        /// <summary>
        /// The annotation is associated with the selected log entry
        /// </summary>
        public ICommand UpdateAnnotationCommand { get; }
        /// <summary>
        /// The annotation is associated with a log entry received as a parameter
        /// </summary>
        public ICommand UpdateAnnotationCommandExternal { get; }
        /// <summary>
        /// The annotation being deleted is associated with the selected log entry
        /// </summary>
        public ICommand DeleteAnnotationCommand { get; }
        /// <summary>
        /// The annotation being deleted is associated with a log entry received as a parameter
        /// </summary>
        public ICommand DeleteAnnotationCommandExternal { get; }

        public ConditionalCommand PreviousAnnotationCommand { get; }
        public ConditionalCommand NextAnnotationCommand { get; }
        public ConditionalCommand DeleteAllAnnotationsCommand { get; }

        public MainWindowViewModel() {

            DebugLog("MainWindowViewModel started");

            _allLogEntries.CollectionChanged += OnAllLogEntriesChanged;

            AbstractLogColumnViewModel.ColumnCallback = this;

            _columnsInfo = new AbstractLogColumnViewModel[5];
            ProcessColumn = new ProcessLogColumnViewModel();
            AreaColumn = new AreaLogColumnViewModel();
            CategoryColumn = new CategoryLogColumnViewModel();
            EventColumn = new EventLogColumnViewModel();
            LevelColumn = new LevelLogColumnViewModel();


            CloseCommand = new ExecutableCommand(OnClose);
            LoadCommand = new ExecutableCommand(OnLoad);
            OpenCommand = new ExecutableCommand(OnOpen);
            SaveCommand = new ExecutableCommand(OnSave);

            MessageFilterCommand = new ExecutableCommand(OnFilterMessage);
            ClearMessageFilterCommand = new ExecutableCommand(OnClearMessageFilter);
            CopyMessageCommand = new ExecutableCommand(OnCopyCurrentMessage);

            CorrelationFilterCommand = new ExecutableCommand(OnFilterMessage);
            ClearCorrelationFilterCommand = new ExecutableCommand(OnClearCorrelationFilter);
            IncludeCorrelationCommand = new ExecutableCommand(OnIncludeCurrentCorrelation);
            CopyCorrelationCommand = new ExecutableCommand(OnCopyCurrentCorrelation);
            HighlightCorrelationCommand = new ExecutableCommand(OnHighlightCurrentCorrelation);

            CopyThreadIdCommand = new ExecutableCommand(OnCopyCurrentThreadId);

            FocusToMessageSearchBoxCommand = new ExecutableCommand(OnFocusToMessageSearchBox);
            ManageComplexFiltersCommand = new ExecutableCommand(OnManageComplexFilters);

            JumpToEntryCommand = new ExecutableCommand(OnJumpToEntry);
            AboutCommand = new ExecutableCommand(OnAbout);

            //MessageSearchCommand = new ConditionalCommand(OnSearchForwardsForMessage, () => !String.IsNullOrEmpty(MessageSearch));
            //MessageSearchPreviousCommand = new ConditionalCommand(OnSearchBackwardsForMessage, () => !String.IsNullOrEmpty(MessageSearch));
            MessageSearchCommand = new ConditionalCommand(TagSearchMessage, () => !String.IsNullOrEmpty(MessageSearch));
            MessageSearchPreviousCommand = new ConditionalCommand(OnGoToPreviousSearchAnnotation, CanGoToPreviousSearchAnnotation);
            MessageSearchNextCommand = new ConditionalCommand(OnGoToNextSearchAnnotation, CanGoToNextSearchAnnotation);
            DeleteAllSearchMarksCommand = new ConditionalCommand(OnDeleteAllSearchAnnotations, CanDeleteAllSearchAnnotations);
            DisplayTimeStartPopupCommand = new ExecutableCommand(OnDisplayTimeStartPopup);
            DisplayShowColumnsPopupCommand = new ExecutableCommand(OnDisplayShowColumnsPopup);
            DisplayShowOthersPopupCommand = new ExecutableCommand(OnDisplayOthersPopup);

            ManageColorsCommand = new ExecutableCommand(OnManageColors);

            ApplyTimelineSelectionFilterCommand = new ConditionalCommand(OnFilterByTimeline, CanFilterByTimeline);
            ApplyTimeIntervalCommand = new ConditionalCommand(OnApplyTimeInterval, CanApplyTimeInterval);
            ShowFullTimeIntervalCommand = new ConditionalCommand(OnShowFullTimeInterval, CanShowFullTimeInterval);

            CopyTextCommand = new ConditionalCommand(OnCopySelectedMessageText, CanCopySelectedMessageText);
            SearchTextCommand = new ConditionalCommand(OnSearchSelectedMessageText, CanSearchSelectedMessageText);

            CopyDateCommand       = new ExecutableCommand(OnCopyCurrentDate);
            CopyLineCommand = new ExecutableCommand(OnCopyCurrentLogLine);

            ExcludeLinesBeforeCommand = new ExecutableCommand(OnExcludeEntriesBefore);
            ExcludeLinesAfterCommand  = new ExecutableCommand(OnExcludeEntriesAfter);
    
            DropReplaceFilesCommand = new ExecutableCommand(OnDropReplaceFiles);
            DropAddFilesCommand = new ExecutableCommand(OnDropAddFiles);

            CancelOperationCommand = new ExecutableCommand(OnCancelOperation);

            TimelineGroups = new List<TimelineInfo>();

            LogCommandBindings = new CommandBindingCollection {
                    new CommandBinding(ApplicationCommands.Copy,
                                       OnCopySelectedLogLine,
                                       OnCanCopySelectedLine),
            };

            ChangeAllFilterIcons();

            SetupComplexFilters();

            ToolbarColumnWidths = new ObservableCollection<double>();

            RecentSearchMessages = new ObservableCollection<string>();
            foreach (string s in App.GlobalSettings.RecentSearchMessages) {
                RecentSearchMessages.Add(s);
            }
            RecentSearchFilters = new ObservableCollection<string>();
            foreach (string s in App.GlobalSettings.RecentSearchMessages) {
                RecentSearchFilters.Add(s);
            }

            SelectedLogEntries = new List<LogEntry>();

            SetupColorInfrastructure();

            _uiDispatcher = Dispatcher.CurrentDispatcher;

            Annotations = new SortingObservableCollection<AnnotationInfo, int>(Comparer<int>.Default, model => model.StartPercent);
            UpdateAnnotationCommand = new ExecutableCommand(OnUpdateAnnotation);
            UpdateAnnotationCommandExternal = new ExecutableCommand(OnUpdateAnnotationExternal);
            DeleteAnnotationCommand = new ConditionalCommand(OnDeleteAnnotation, CanDeleteAnnotation);
            DeleteAnnotationCommandExternal = new ExecutableCommand(OnDeleteAnnotationExternal);
            NextAnnotationCommand = new ConditionalCommand(OnGoToNextUserAnnotation, CanGoToNextUserAnnotation);
            PreviousAnnotationCommand = new ConditionalCommand(OnGoToPreviousUserAnnotation, CanGoToPreviousUserAnnotation);
            DeleteAllAnnotationsCommand = new ConditionalCommand(OnDeleteAllUserAnnotations, CanDeleteAllUserAnnotations);
            DebugLog("MainWindowViewModel ready");
        }

        private bool CanGoToNextUserAnnotation() {

            return Annotations.Any(ai => ai.IsUserAnnotation);
        }
        private bool CanGoToPreviousUserAnnotation() {

            return Annotations.Any(ai => ai.IsUserAnnotation);
        }

        private bool CanDeleteAllUserAnnotations() {

            return Annotations.Any(ai => ai.IsUserAnnotation);
        }

        private void OnDeleteAllUserAnnotations() {

            IMessageToUser ui = ServiceRegistry.Resolve<IMessageToUser>();
            if (ui.AskQuestion(Title, "Are you sure that you want to delete all annotations?", QuestionType.YesNo) == AnswerType.Yes) {

                IEnumerable<AnnotationInfo> toBeDeleted = Annotations.Where(ai => ai.IsUserAnnotation).ToList();

                foreach (var ai in toBeDeleted) {
                    DoDeleteAnnotation(ai, true);
                }
            }
        }

        private void OnGoToNextUserAnnotation() {

            if (SelectedLogEntry == null) {
                return;
            }

            // as annotations are sorted by time (through the StartPercent property), the first one is what we need
            AnnotationInfo laterAnnotation = Annotations.FirstOrDefault(ai => ai.IsUserAnnotation && ai.AssociatedEntry.LineNumber> SelectedLogEntry.LineNumber);

            if (laterAnnotation == null) {
                // we are at the last one (there should be at least one because of CanGoToNextUserAnnotation)
                laterAnnotation = Annotations[0];
            }
            GotoEntry(laterAnnotation.AssociatedEntry);
        }

        private void OnGoToPreviousUserAnnotation() {

            if (SelectedLogEntry == null) {
                return;
            }

            // as annotations are sorted by time (through the StartPercent property), the last one is what we need
            AnnotationInfo earlierAnnotation = Annotations.LastOrDefault(ai => ai.IsUserAnnotation && ai.AssociatedEntry.LineNumber < SelectedLogEntry.LineNumber);

            if (earlierAnnotation == null) {
                // we are at the first one
                earlierAnnotation = Annotations[Annotations.Count - 1];
            }
            GotoEntry(earlierAnnotation.AssociatedEntry);
        }

        private static void DebugLog(string text, params object[] parameters) {
            App.DebugLog(text, parameters);
        }

        private void OnCanCopySelectedLine(object sender, CanExecuteRoutedEventArgs e) {
            e.CanExecute = _selectedLogEntry != null;
        }

        private void OnCopyCurrentLogLine() {

            DebugLog("User asked to copy the current line(s)");

            StringBuilder sb = new StringBuilder(5000);

            if (GetSelection != null) {
                IList selectedItems = GetSelection.SelectedItems;

                if (selectedItems.Count > 1) {

                    DebugLog("Copying {0} lines", selectedItems.Count);
                    
                    // Header
                    sb.Append("Date\t");
                    sb.Append("Process\t");
                    sb.Append("Thread Id\t");
                    sb.Append("Area\t");
                    sb.Append("Category\t");
                    sb.Append("Event Id\t");
                    sb.Append("Level\t");
                    sb.Append("Correlation\t");
                    sb.AppendLine("Message");

                    // lines
                    foreach (LogEntry le in selectedItems) {
                        sb.AppendFormat("{0}\t", le.DisplayedDay);
                        sb.AppendFormat("{0}\t", le.Process);
                        sb.AppendFormat("{0}\t", le.Tid);
                        sb.AppendFormat("{0}\t", le.Area);
                        sb.AppendFormat("{0}\t", le.Category);
                        sb.AppendFormat("{0}\t", le.EventID);
                        sb.AppendFormat("{0}\t", le.Level);
                        sb.AppendFormat("{0}\t", le.Correlation);
                        sb.AppendLine(le.Message);
                    }
                    Clipboard.SetText(sb.ToString());
                    return;
                }
            } 

            LogEntry entry = SelectedLogEntry;
            if (entry == null) {

                DebugLog("But there is no current line");
                return;
            }

            sb.AppendFormat("Date:\t{0}", entry.DisplayedDay);
            sb.AppendLine();
            sb.AppendFormat("Process:\t{0}", entry.Process);
            sb.AppendLine();
            sb.AppendFormat("Thread Id: {0}", entry.Tid);
            sb.AppendLine();
            sb.AppendFormat("Area: {0}", entry.Area);
            sb.AppendLine();
            sb.AppendFormat("Category: {0}", entry.Category);
            sb.AppendLine();
            sb.AppendFormat("Event Id: {0}", entry.EventID);
            sb.AppendLine();
            sb.AppendFormat("Level: {0}", entry.Level);
            sb.AppendLine();
            sb.AppendFormat("Correlation:{0}", entry.Correlation);
            sb.AppendLine();
            sb.AppendLine("Message:");
            sb.AppendLine(entry.ParsedMessage);
            Clipboard.SetText(sb.ToString());
            
        }

        private void OnCopySelectedLogLine(object sender, ExecutedRoutedEventArgs e) {
            OnCopyCurrentLogLine();
        }

        private void OnFocusToMessageSearchBox() {
            IsMessageSearchBoxFocused = true;
        }

        private static List<LogFileInfo> s_externalChoiceOfFilesToRead;
        private static DateTime s_externalSelectedDate;

        public static void SetInitialFilesToRead(List<LogFileInfo> filesToRead, DateTime selectedDate ) {
            s_externalChoiceOfFilesToRead = filesToRead;
            s_externalSelectedDate = selectedDate;
        }

        private List<LogFileInfo> AskUserForLogFilesToRead() {

            IUIVisualizer viz = ServiceRegistry.Resolve<IUIVisualizer>();
            LogFileChooserViewModel lfvm = new LogFileChooserViewModel();
            lfvm.ChooseAdditionOrReplaceVisibility = _allLogEntries.Count > 0
                                                             ? Visibility.Visible
                                                             : Visibility.Collapsed;
            lfvm.IsWindowVisibleInTaskbar = false;

            if (viz.ShowDialog(App.FileChooser, lfvm) == false) {

                return null;
            }
            
            _userChoseToReplaceLogFile = lfvm.IsLogReplacement;
            _userChoseToGoAtThisTime = lfvm.UserSelectedDate;

            return lfvm.SelectedLogFiles;
        }

        private void OnLoad() {

            string[] args = Environment.GetCommandLineArgs();

            _filesToRead = null;

            if (args.Length > 1) {

                // skip 1st arg, which is the path to the exe, get a FileInfo, remove non-existing files
                _filesToRead = args.Skip(1).Select(s => new FileInfo(s)).Where(fi => fi.Exists).Select(fi => new LogFileInfo(fi)).ToList();

                if (_filesToRead.Count == 0) {
                    // we got invalid file names as parameters => ask
                    _filesToRead = AskUserForLogFilesToRead();
                }
            }

            if (_filesToRead == null || _filesToRead.Count == 0) {

                _filesToRead = s_externalChoiceOfFilesToRead;
                _userChoseToGoAtThisTime = s_externalSelectedDate;
            }


            if (_filesToRead == null) {

                DebugLog("User cancelled chooser dialog; exiting");
                Application.Current.Shutdown();
                return;
            }

            SortByDate(_filesToRead);
            StartWatch();
            ReadFiles();

            OnPropertyChanged("IsDateShown");

            ParsedMessageSelectedText = String.Empty;
        }

        private void OnSave() {

            IBrowseFileSystem bfs = ServiceRegistry.Resolve<IBrowseFileSystem>();

            string newSaveFile;
            string defaultPath = null;
            if (_filesInMemory.Count > 0) {
                defaultPath = _filesInMemory[_filesInMemory.Count - 1].OriginalFile.DirectoryName;
            }
            if (bfs.AskForFileToSave("Select a file to save the current log to", "Log file", "log", defaultPath, out newSaveFile)) {
                _lr.SaveLogFile(newSaveFile, _filteredLogEntries, ErrorHandler, OnEndSave);

                Status = "Saving...";
            }
        }

        private void OnEndSave() {
            Status = "Idle";
        }

        private void OnOpen() {

            DisableFileWatching();

            List<LogFileInfo> newFilesToRead = AskUserForLogFilesToRead();

            if (newFilesToRead != null) {

                _filesToRead = newFilesToRead;

                _lr.CancelCurrentTask();
                HandleChosenFilesWhenReaderIsIdle();
            }
        }

        private void HandleChosenFilesWhenReaderIsIdle() {

            // cannot read new files if other files are being read
            if (_lr.IsBusy) {

                _uiDispatcher.BeginInvoke(new Action (HandleChosenFilesWhenReaderIsIdle),
                                          DispatcherPriority.Background);
                return;
            }
            HandleChosenFiles();
        }

        private void HandleChosenFiles() {
            SortByDate(_filesToRead);

            if (_userChoseToReplaceLogFile) {
                ResetAllInfo();
                StartWatch();
                ReadFiles();
            } else {
                StartWatch();
                ReadPotentiallyOutOfOrderFiles();
            }
        }

        private void SortByDate(List<LogFileInfo> files ) {
            
            files.Sort((info1, info2) => info1.DateTime.CompareTo(info2.DateTime));
        }

        private void ResetAllInfo() {

            _allLogEntries.Clear();
            lock (_filteredLogLock) {
                _filteredLogEntries.Clear();
            }
            foreach (var ci in _columnsInfo) {
                ci.ResetInfo();
            }
            ChangeAllFilterIcons();

            LogReader.ResetDayStatistics();

            Annotations.Clear();

            _filesInMemory.Clear();
        }

        private void ChangeAllFilterIcons() {

            foreach (var ci in _columnsInfo) {

                ci.ChangeFilterIcons();
            }
        }

        /// <summary>
        /// Read files in _filesToRead, but orders them if needed with _filesInMemory
        /// </summary>
        private void ReadPotentiallyOutOfOrderFiles() {
         
            // if all files in _filesToRead are alreay 'later' than the files in _filesInMemory,
            // we can read them normally.
            // other we need to reread everything

            // we may have been called several times
            if (_filesToRead.Count == 0) {
                return;
            }

            // as both lists are sorted, it is enough to compare the first new file with the last read files
            if (_filesInMemory.Count == 0 || _filesToRead[0].DateTime > _filesInMemory[_filesInMemory.Count - 1].DateTime) {
                
                ReadFiles();
            } else {

                foreach (LogFileInfo lfi in _filesInMemory) {

                    if (!_filesToRead.Contains(lfi)) {
                        _filesToRead.Add(lfi);
                    }
                }

                ResetAllInfo();
                SortByDate(_filesToRead);
                ReadFiles();
            }
        }

        private void ReadFiles() {

            if (_filesToRead.Count > 0) {

                LogFileInfo lfi = _filesToRead[0];
                _filesToRead.RemoveAt(0);

                // mae sure that file still exists
                lfi.OriginalFile.Refresh();
                if (lfi.OriginalFile.Exists) {
                    _filesInMemory.Add(lfi);
                    Status = $"Reading {lfi.OriginalFile.Name}...";

                    DebugLog("About to read {0}", lfi.OriginalFile.FullName);
                    _lr.ReadLogFile(lfi.OriginalFile.FullName, AddLogEntries, App.GlobalSettings.IsDuplicateRemovalEnabled,
                                    ErrorHandler, ReadFiles);
                } else {
                    ReadFiles(); // continue reading with the next file
                }
            } else {
                Status = "Idle";
                StopWatch();

                DebugLog("Finished reading file(s)");
                RememberLengthOfLastFileRead();
                EnableFileWatching();

                if (_userChoseToGoAtThisTime != DateTime.MinValue) {
                    ShowTime(_userChoseToGoAtThisTime);
                }

                RecomputeTimelineGroupsIfNeeded();
            }

            // show line information after each file read
            UpdateLineInformation();
            SetupDateInformation();
            DeserializeAnnotations();
            UpdateFileInformation();
        }

        private void ShowTime(DateTime time) {

            DebugLog("Starting ShowTime");

            foreach (LogEntry le in _filteredLogEntries) {
                
                if (le.Day >= time) {
                    GotoEntry(le);
                    break;
                }
            }
            DebugLog("End of ShowTime");
        }

        private void GotoEntry(LogEntry le) {
            LogEntryToShow = le;
            SelectedLogEntry = le;
        }

        private void UpdateFileInformation() {
            DisplayedFilesList = "from " + String.Join(", ", _filesInMemory.Select(lfi => lfi.OriginalFile.Name).ToArray());
        }

        private void AddLogEntries(IList<LogEntry> items, int count) {

            DebugLog("AddLogEntries: got {0} entries", count);

            for (int i = 0; i < count; i++) {

                LogEntry item = items[i];
                _allLogEntries.Add(item);
            }

            UpdateLineInformation();
            DebugLog("End of AddLogEntries");
        }

        private void UpdateLineInformation() {
            OnPropertyChanged("LineInformation");
        }

        private void OnClose() {

            App.GlobalSettings.MainTextBoxHeight = DetailBarHeight.Value; // we know the unit is pixel

            // take the last 10 search strings
            App.GlobalSettings.RecentSearchMessages.Clear();
            App.GlobalSettings.RecentSearchMessages.AddRange(RecentSearchMessages.Reverse().Take(10));

            // take the last 10 filter strings
            App.GlobalSettings.RecentSearchFilters.Clear();
            App.GlobalSettings.RecentSearchFilters.AddRange(RecentSearchFilters.Reverse().Take(10));

            _lr.Dispose();
            if (_lastFileWatcher != null) {
                _lastFileWatcher.EnableRaisingEvents = false;
                _lastFileWatcher.Dispose();
            }
            if (_newFileWatcher != null) {
                _newFileWatcher.EnableRaisingEvents = false;
                _newFileWatcher.Dispose();
            }

            Application.Current.Shutdown();
        }

        private void SetupDateInformation() {

            if (_filteredLogEntries.Count == 0) {
                return;
            }

            _minAvailableDate = _minChosenDate = LogReader.MinDate;
            _maxAvailableDate = _maxChosenDate = LogReader.MaxDate;

            if (String.IsNullOrEmpty(ChosenMiddleLogTime)) {

                TimeSpan timeSpan = _maxAvailableDate - _minAvailableDate;
                DateTime middle = _minAvailableDate + TimeSpan.FromSeconds(timeSpan.TotalSeconds / 2);
                ChosenMiddleLogTime = middle.ToDisplayParsableString();
            }

            OnPropertyChanged("DisplayedMinAvailableLogDate");
            OnPropertyChanged("DisplayedMaxAvailableLogDate");

            UpdateAnnotationPositions();
        }

        private void ErrorHandler(string text, string filePath) {
            MessageBox.Show(text, Title, MessageBoxButton.OK, MessageBoxImage.Error);

            // in case of a read error, forget about the file
            LogFileInfo lfi = _filesInMemory.FirstOrDefault(info => info.OriginalFile.FullName == filePath);
            if (lfi != null) {
                _filesInMemory.Remove(lfi);
            }
        }

        private void OnAllLogEntriesChanged(object sender, NotifyCollectionChangedEventArgs e) {

            if (e.Action == NotifyCollectionChangedAction.Add) {

                foreach (LogEntry le in e.NewItems) {

                    // if we are filtering by any column and the entry has an unknown value in this column, we do not display it
                    bool isKnown = true;

                    foreach (var ci in _columnsInfo) {

                        ci.RememberItem(le, ref isKnown);
                    }

                    if (isKnown && IsAcceptedByFilters(le)) {

                        ComputeColorCode(le, false);
                        _filteredLogEntries.Add(le);
                        le.LineNumber = _filteredLogEntries.Count;
                    }
                }
            }
        }

        private bool IsAcceptedByFilters(LogEntry le) {

            if (_columnsInfo.Any(ci => ci.IsFilteredOut(le))) {
                return false;
            }

            if (_isMessageFilterNonEmpty) {

                if (_messageRegex != null) {
                    if (!_messageRegex.IsMatch(le.LowerCaseMessage)) {
                        return false;
                    }
                } else {
                    if (!le.LowerCaseMessage.Contains(_lowerCaseMessageFilter)) {
                        return false;
                    }
                }
            }

            if (_isCorrelationFilterNonEmpty) {
                if (! le.LowerCaseCorrelation.Contains(_lowerCaseCorrelationFilter)) {
                    return false;
                }
            }

            foreach (ComplexFilter cf in App.GlobalSettings.ComplexFilters) {
            
                if (cf.IsMatch(le)) {
                    return false;
                }
            }

            return true;
        }

        private readonly Stopwatch _watch = new Stopwatch();

        private void StartWatch() {

            _watch.Reset();
            _watch.Start();
        }

        private void StopWatch() {

            _watch.Stop();
#if DEBUG
            long ts = _watch.ElapsedMilliseconds;
            Status = "Took: " + ts.ToString("G");
#endif
        }

        private void OnCancelOperation() {

            foreach (var ci in _columnsInfo) {
                ci.CancelOperation();
            }
            if (IsShowingDropDialog) {
                OnCancelDrop();
            }
        }

        private readonly List<FilterChoice> _tempFilterChoices = new List<FilterChoice>();

        private void OnFilterMessage() {
            if (SetupMessageFiltering()) {
                FilterAllEntries();
            }
        }

        private void OnClearMessageFilter() {
            DebugLog("User asked to clear the message filter");
            MessageFilter = String.Empty;
            OnFilterMessage();
        }

        private void OnClearCorrelationFilter() {
            _isCorrelationFilterNonEmpty = false;
            FilterAllEntries();
        }

        private void SetAllChoices(IEnumerable<FilterChoice> choicesList, bool newValue) {
            foreach (FilterChoice fc in choicesList) {
                fc.IsSelected = newValue;
            }
        }

        private Regex _messageRegex;
        private bool SetupMessageFiltering() {
            
            if (! RecentSearchFilters.Contains(MessageFilter) && !String.IsNullOrEmpty(MessageFilter)) {
                RecentSearchFilters.Add(MessageFilter);
            }

            if (IsMessageFilterRegex && _isMessageFilterNonEmpty) {
                try {
// ReSharper disable PossibleNullReferenceException
                    _messageRegex = new Regex(MessageFilter.ToLower(CultureInfo.CurrentCulture), RegexOptions.Compiled);
// ReSharper restore PossibleNullReferenceException
                } catch (ArgumentException) {

                    IMessageToUser msg = ServiceRegistry.Resolve<IMessageToUser>();
                    msg.ShowError(Title, "Regular expression is invalid; please correct it.");

                    return false;
                }
            } else {
                _messageRegex = null;
            }

            return true;
        }

        private void InitVisibiltyComputation(){
            foreach (var ci in _columnsInfo) {
                ci.InitVisibiltyComputation();
            }
        }

        /// <summary>
        /// add data in <paramref name="le"/> to the list of visible elements
        /// </summary>
        /// <param name="le"></param>
        private void UpdateVisibilityComputation(LogEntry le){

            foreach (var ci in _columnsInfo) {
                ci.UpdateVisibilityComputation(le);
            }
        }

        private void UpdateFilterVisibility(){

            foreach (var ci in _columnsInfo) {
                ci.UpdateFilterVisibility();
            }
        }

        public void FilterAllEntries() {

            DebugLog("Starting FilterAllEntries");
//            int i = 0;
            Stopwatch filterWatch = new Stopwatch();
            filterWatch.Start();

            InitVisibiltyComputation();

            using (new WaitingCursor()) {

                LogEntry oldSelection = SelectedLogEntry;

                // it is much faster to store the new list of filtered entries in a separate collection
                // and then assign the new collection at once.
                // on a "slow" machine, with about 90K entries, the previous method was taking about 1 s.
                // This new method needs 100 ms.
                ObservableCollection<LogEntry> tempLogEntries = new ObservableCollection<LogEntry>();

                foreach (LogEntry le in _allLogEntries) {

//                    i++;
//                    if (i >= 10000) {
//                        DebugLog("FilterAllEntries: handled 10'000 entries");
//                        i = 0;
//                    }

                    // these 2 tests cannot be in IsAcceptedByFilters, because
                    // new lines added by the auto-refresh are usually later than MaxChosenDate
                    // => would never be displayed
                    // same for min, if user adds new files at the beginning

                    bool isEntryVisible = le.Day >= _minChosenDate &&
                                          le.Day <= _maxChosenDate &&
                                          IsAcceptedByFilters(le);

                    UpdateAnnotationVisibility(le, isEntryVisible);

                    if (isEntryVisible) {
                        ComputeColorCode(le, false);
                        tempLogEntries.Add(le);
                        le.LineNumber = tempLogEntries.Count;

                        UpdateVisibilityComputation(le);
                    }
                }

                CanvasTransformer.ForgetAboutCollection(_filteredLogEntries);

                lock (_filteredLogLock) {
                    _filteredLogEntries = tempLogEntries;
                }
                OnPropertyChanged("DisplayedLogEntries");

                if (oldSelection != null) {
                    GotoEntry(oldSelection);
                }

                UpdateFilterVisibility();
            }

            UpdateLineInformation();

            RecomputeTimelineGroupsIfNeeded();

            // filtering entries now has repercussions on all filter definitions => recompute the icon/tooltip of each
            ChangeAllFilterIcons();

            UpdateAnnotationPositions();
            filterWatch.Stop();
            DebugLog("Finished FilterAllEntries; it took {0} ms", filterWatch.ElapsedMilliseconds);
        }

        private void SetupComplexFilters() {

            foreach (ComplexFilter cf in App.GlobalSettings.ComplexFilters) {
                cf.SetupComparison();
            }

            ManageComplexFilterCaption = App.GlobalSettings.ComplexFilters.Count == 0
                                                 ? "Exclusion filters..."
                                                 : String.Format("Exclusion filters ({0} active)...",
                                                                 App.GlobalSettings.ComplexFilters.Count);
        }

        private void OnManageComplexFilters() {
            
            // make a copy in case the user cancels
            ObservableCollection<ComplexFilter> tempFilters = new ObservableCollection<ComplexFilter>();
            foreach (ComplexFilter cf in App.GlobalSettings.ComplexFilters) {

                tempFilters.Add(cf.Clone());
            }

            IUIVisualizer viz = ServiceRegistry.Resolve<IUIVisualizer>();

            ComplexFilterViewModel cfvm = new ComplexFilterViewModel();
            cfvm.Filters = tempFilters;
            if (SelectedLogEntry != null) {
                cfvm.TestMessage = SelectedLogEntry.Message;
            }

            if (viz.ShowDialog(App.ComplexFilterManager, cfvm) == true) {
                App.GlobalSettings.ComplexFilters.Clear();
                App.GlobalSettings.ComplexFilters.AddRange(tempFilters);

                SetupComplexFilters();
                FilterAllEntries();
            }
        }

        void IDropTarget.HandleDragOver(DragEventArgs dea) {

            bool acceptable = dea.Data.GetDataPresent(DataFormats.FileDrop);

            if (acceptable) {

                string[] data = (string[])dea.Data.GetData(DataFormats.FileDrop);

                // can only be files for us
                foreach (string path in data) {

                    FileInfo fi = new FileInfo(path);
                    if (!fi.Exists) {
                        acceptable = false;
                        break;
                    }
                }
            }

            dea.Handled = true;
            dea.Effects = acceptable ? 
                ((dea.KeyStates & DragDropKeyStates.ShiftKey) == DragDropKeyStates.ShiftKey ? DragDropEffects.Move : DragDropEffects.Copy) : 
                DragDropEffects.None;
        }

        void IDropTarget.HandleDrop(DragEventArgs dea) {

            string[] data = (string[])dea.Data.GetData(DataFormats.FileDrop);

            _filesToRead.Clear();
            foreach (string path in data) {

                FileInfo fi = new FileInfo(path);
                _filesToRead.Add(new LogFileInfo(fi));
            }

            if (dea.KeyStates == DragDropKeyStates.ControlKey) {
                OnDropAddFiles();
            } else if (dea.KeyStates == DragDropKeyStates.ShiftKey) {
                OnDropReplaceFiles();
            } else {
                FilesDropCount = _filesToRead.Count;
                IsShowingDropDialog = true;
            }

            dea.Handled = true;
        }

        private void OnDropReplaceFiles() {

            IsShowingDropDialog = false;
            _userChoseToReplaceLogFile = true;
            HandleChosenFiles();
        }

        private void OnDropAddFiles() {

            IsShowingDropDialog = false;
            _userChoseToReplaceLogFile = false;
            HandleChosenFiles();
        }

        private void OnCancelDrop() {
            _filesToRead.Clear();
            IsShowingDropDialog = false;
        }

        private void DisableFileWatching() {
            
            if (_lastFileWatcher != null) {
                _lastFileWatcher.EnableRaisingEvents = false;
            }
            if (_newFileWatcher != null) {
                _newFileWatcher.EnableRaisingEvents = false;
            }
        }

        private void RememberLengthOfLastFileRead() {
            
            if (_filesInMemory.Count == 0) {
                return;
            }

            FileInfo lastFileInfo = _filesInMemory[_filesInMemory.Count - 1].OriginalFile;
            DirectoryInfo di = lastFileInfo.Directory;

            if (di == null) {
                return;
            }

            // Fixed issue in 0.6: a reload would read again and again all added lines since first read, because _lastFileReadOffset never changed.
            lastFileInfo.Refresh();
            // assumes that we could read the whole file
            _lastFileReadOffset = lastFileInfo.Length;
        }

        private void EnableFileWatching() {

            if (_isAutoRefresh) {

                if (_filesInMemory.Count == 0) {
                    return;
                }

                FileInfo lastFileInfo = _filesInMemory[_filesInMemory.Count - 1].OriginalFile;
                DirectoryInfo di = lastFileInfo.Directory;

                if (di == null) {
                    return;
                }

                // set up taking account of changes in the last open file
                long oldFileLength = lastFileInfo.Length;
                lastFileInfo.Refresh();

                if (_lastFileWatcher != null) {

                    _lastFileWatcher.EnableRaisingEvents = false;
                    _lastFileWatcher.Path = di.FullName;
                } else {
                    _lastFileWatcher = new FileSystemWatcher(di.FullName);
                    _lastFileWatcher.Changed += OnLastFileChanged;
                }

                _lastFileWatcher.Filter = lastFileInfo.Name;
                _lastFileWatcher.NotifyFilter = NotifyFilters.Size;
                _lastFileWatcher.EnableRaisingEvents = true;

                if (oldFileLength != lastFileInfo.Length) {

                    StartFileReadBecauseOfChange(lastFileInfo.FullName);
                }

                // set up taking account of the arrival of a new log file
                if (_newFileWatcher != null) {

                    _newFileWatcher.EnableRaisingEvents = false;
                    _newFileWatcher.Path = di.FullName;
                } else {
                    _newFileWatcher = new FileSystemWatcher(di.FullName);
                    _newFileWatcher.Created += OnNewFileDetected;
                }

                _newFileWatcher.Filter = LogFileChooserViewModel.LogPattern;
                //_newFileWatcher.NotifyFilter = NotifyFilters.Size; // ??
                _newFileWatcher.EnableRaisingEvents = true;

            }
        }

        private void OnNewFileDetected(object sender, FileSystemEventArgs e) {
            
            DebugLog("Got notification of file arrival");

            string path = e.FullPath;
            // go back to main thread
            _uiDispatcher.BeginInvoke(new Action(() => PotentiallyAddFile(path)));
        }

        private static bool s_isHandlingChangeNotification;

        private void OnLastFileChanged(object sender, FileSystemEventArgs e) {
     
            // size has changed

            DebugLog("Got change size notification");
            StartFileReadBecauseOfChange(e.FullPath);
        }

        private void StartFileReadBecauseOfChange(string filePath) {
            
            if (s_isHandlingChangeNotification) {

                DebugLog("Change size notification ignored because we are still handling the previous one");
                return;
            }

            if (_lr.IsBusy) {
                
                DebugLog("Change size notification ignored because the reader is busy");
                return;
            }

            s_isHandlingChangeNotification = true;
            DebugLog("Refreshing {0}, starting at offset {1}", filePath, _lastFileReadOffset);
            // same as a normal read, but starting after the part already read
            _lr.ReadLogFile(filePath, AddLogEntries, App.GlobalSettings.IsDuplicateRemovalEnabled, 
                            ErrorHandler, EndReadFile, _lastFileReadOffset);
        }

        private void PotentiallyAddFile(string filePath) {

            FileInfo fi = new FileInfo(filePath);
            if (fi.Exists) {
                
                LogFileInfo lfi = new LogFileInfo(fi);
                if (lfi.IsLikelyASharePointLogFile) {
                    _filesToRead.Add(lfi);
                    _userChoseToReplaceLogFile = false;
                    HandleChosenFilesWhenReaderIsIdle();
                }
            }
        }

        private void EndReadFile() {
            s_isHandlingChangeNotification = false;

            DebugLog("EndReadFiles");
            ReadFiles();

            if (_isAutoScrolling) {
                // do not let _filteredLogEntries change while we read its count and move to the last one
                lock (_filteredLogLock) {
                    if (_filteredLogEntries.Count > 0) {
                        GotoEntry(_filteredLogEntries[_filteredLogEntries.Count - 1]);
                    }
                }
            }
        }

        private string GetTimelineField(LogEntry le) {

            switch (TimelineSelectedIndex) {
                
                case TimelineProcessIndex:
                    return le.Process;

                case TimelineCategoryIndex:
                    return le.Category;

                case TimelineLevelIndex:
                    return le.Level;

                case TimelineAreaIndex:
                    return le.Area;

                case TimelineThreadIDIndex:
                    return le.Tid;

            }

            throw new ArgumentException("Invalid timeline type", nameof(le));
        }

        private void RecomputeTimelineOnPropertyChanged() {
            ResetTimelineAllocation();

            RecomputeTimelineGroupsIfNeeded();
        }

        private void RecomputeTimelineGroupsIfNeeded() {
            // only recompute if the timeline is visible
            if (_isTimelineToBeShown) {
                ComputeTimelineGroups();
            } else {
                // just reset the width info, so that when the user asks for seeing the timeline, it is recomputed (see IsTimelineToBeShown)
                TimelineGroupsWidth = 0;
            }
        }

        private static readonly Color[] s_availableColors = new Color[] {
                Colors.Green,
                Colors.Yellow,
                Colors.Blue,
                Colors.Gray,
                Colors.Turquoise,
                Colors.Orange,
                Colors.LightBlue,
                Colors.Lime,
                Colors.DarkGray,
                Colors.HotPink,
                Colors.NavajoWhite,
                Colors.DarkOliveGreen,
                Colors.BlueViolet,
                Colors.Red,
                Colors.YellowGreen,
                Colors.Beige
        };

        private static readonly Dictionary<string, int> s_allocatedColors = new Dictionary<string, int>(20);
        private static readonly SolidColorBrush[] s_brushes = new SolidColorBrush[s_availableColors.Length];
        private static readonly Dictionary<string, SolidColorBrush> s_levelBrushes = new Dictionary<string, SolidColorBrush> {
                { "Unexpected", new SolidColorBrush(Colors.Black) },
                { "Monitorable", new SolidColorBrush(Colors.LightBlue)},
                { "Medium", new SolidColorBrush(Colors.Gray)},
                { "High", new SolidColorBrush(Colors.Red)},
                { "Verbose", new SolidColorBrush(Colors.Green)},
                { "Information", new SolidColorBrush(Colors.BlueViolet)},
                { "Warning", new SolidColorBrush(Colors.Yellow)},
                { "Error", new SolidColorBrush(Colors.Orange)},
                { "Critical", new SolidColorBrush(Colors.White)},
        };

        // the number of rectangles for each reference value (a value is identified by its brush)
        private static readonly Dictionary<Brush, long> s_groupRectanglesCount = new Dictionary<Brush, long>(20);

        private void ResetTimelineAllocation() {
            s_allocatedColors.Clear();
            s_groupRectanglesCount.Clear();
        }

        private SolidColorBrush GetBrushForTimelineData(string text) {

            bool isLevel = TimelineSelectedIndex == TimelineLevelIndex;

            if (isLevel) {

                SolidColorBrush result;
                if (s_levelBrushes.TryGetValue(text, out result)) {

                    s_groupRectanglesCount[result] = 0;
                    return result;
                }

                // we should not continue here...
            }

            bool hasAllocatedBrush = false;
            int index;
            if (! s_allocatedColors.TryGetValue(text, out index)) {
                // new value
                index = s_allocatedColors.Count;
                if (index >= s_availableColors.Length) {
                    // not enough color => choose at random
                    index = Environment.TickCount % s_availableColors.Length;
                }

                s_allocatedColors[text] = index;
                hasAllocatedBrush = true;
            }

            if (s_brushes[index] == null) {
                s_brushes[index] = new SolidColorBrush(s_availableColors[index]);
                s_brushes[index].Freeze();
            }

            if (hasAllocatedBrush) {
                s_groupRectanglesCount[s_brushes[index]] = 0;
            }

            return s_brushes[index];
        }

        private void ComputeTimelineGroups() {

            long totalWidth = 0;

            TimelineGroups.Clear();

            string referenceValue = null;
            LogEntry startEntry = null;

            foreach (LogEntry le in _filteredLogEntries) {

                string interestingInfo = GetTimelineField(le);
                if (interestingInfo == referenceValue) {
                    continue; // same value as previous entry, nothing special to do
                }

                if (startEntry != null) {

                    TimelineInfo ti = new TimelineInfo {
                        Brush = GetBrushForTimelineData(referenceValue),
                        Width = Math.Abs((long) (le.Day - startEntry.Day).TotalMilliseconds),
                        Explanation = String.Format("{0}, from {1} to {2}", referenceValue, startEntry.Day, le.Day),
                        StartTime = startEntry.Day,
                    };
                    ti.CumulatedWidth = totalWidth;
                    totalWidth += ti.Width;

                    TimelineGroups.Add(ti);

                    // remember that there is a rectangle for this value
                    s_groupRectanglesCount[ti.Brush]++;
                }
                startEntry = le;
                referenceValue = interestingInfo;
            }

            // now handle the last batch of entries
            if (startEntry != null) {

                LogEntry lastEntry = _filteredLogEntries[_filteredLogEntries.Count - 1];
                // as in the general case, it is actually impossible to know the time span of an entry.
                // if the last entry is alone, give it a time span of total / (n + 1) where n is the number of entries
                DateTime lastDate = (lastEntry == startEntry) ? startEntry.Day.AddMilliseconds(totalWidth / (TimelineGroups.Count + 1.0)) : lastEntry.Day;
                TimelineInfo tiLast = new TimelineInfo {
                    Brush = GetBrushForTimelineData(referenceValue),
                    Width = Math.Abs((long) (lastDate - startEntry.Day).TotalMilliseconds),
                    Explanation = String.Format("{0}, from {1} to {2}", referenceValue, startEntry.Day, lastDate),
                    StartTime = startEntry.Day,
                };
                tiLast.CumulatedWidth = totalWidth;
                totalWidth += tiLast.Width;

                // don't update the group widths for the last value; useless
                TimelineGroups.Add(tiLast);
            }

            // find the largest reference value 
            long max = 0;
            Brush maxBrush = null;
            foreach (var kvp in s_groupRectanglesCount) {
                
                if (kvp.Value > max) {
                    max = kvp.Value;
                    maxBrush = kvp.Key;
                }
            }
            // now set the IsMostCommon attribute to the first entry with maxBrush
            foreach (var ti in TimelineGroups) {
                if (ti.Brush.Equals(maxBrush)) {
                    ti.IsMostCommonBrush = true;
                    break; // set it only once.
                }
            }

            // we really need to set the width to something different than before, or the drawing will not occur
            if (totalWidth == TimelineGroupsWidth) {
                totalWidth++; // usually, the width is so large than such a small change should not matter
            }
            TimelineGroupsWidth = totalWidth;
        }


        private void OnJumpToEntry(object arg) {

            LogEntry le = (LogEntry) arg;
            GotoEntry(le);
        }

        private void OnAbout() {

            IUIVisualizer viz = ServiceRegistry.Resolve<IUIVisualizer>();
            viz.ShowDialog(App.About, null);
        }

        //private void OnSearchForwardsForMessage() {
        //    SearchForMessage(1);
        //}

        private void TagSearchMessage() {
            
            if (_filteredLogEntries.Count == 0) {
                Status = "No log entry to search";
                return;
            }

            DeleteSearchBookmarks();
            SetupMessageMatch();

            int matchNumber = 0;

            foreach (var le in _filteredLogEntries) {
                
                if (IsMessageMatch(le)) {

                    matchNumber++;

                    AnnotationInfo ai = new AnnotationInfo {
                        AssociatedEntry = le,
                        Annotation = $"match {matchNumber} for {MessageSearch}",
                        IsPersistent = false,
                        Brush = s_searchBrush,
                        //LightColor = s_searchLightColor,
                        //DarkColor = s_searchDarkColor,
                        IsVisible = true,
                        HeightPercent = 1,
                        StartPercent = ComputePercent(le),
                    };
                    AddAnnotation(ai, false);
                }
            } 

            if (matchNumber > 0) {

                Status = "Found text";
                OnGoToNextSearchAnnotation();
            } else {

                Status = "The following specified text was not found: " + MessageSearch;
            }
        }

        private bool CanGoToNextSearchAnnotation() {

            return Annotations.Any(ai => ai.IsSearchAnnotation);
        }
        private bool CanGoToPreviousSearchAnnotation() {

            return Annotations.Any(ai => ai.IsSearchAnnotation);
        }
        private bool CanDeleteAllSearchAnnotations() {

            return Annotations.Any(ai => ai.IsSearchAnnotation);
        }

        private void OnDeleteAllSearchAnnotations() {
            IEnumerable<AnnotationInfo> toBeDeleted = Annotations.Where(ai => ai.IsSearchAnnotation).ToList();

            foreach (var ai in toBeDeleted) {
                Annotations.Remove(ai);
            }
        }

        private void OnGoToNextSearchAnnotation() {

            if (SelectedLogEntry == null) {
                return;
            }

            // as annotations are sorted by time (through the StartPercent property), the first one is what we need
            AnnotationInfo laterAnnotation = Annotations.FirstOrDefault(ai => ai.IsSearchAnnotation && ai.AssociatedEntry.LineNumber > SelectedLogEntry.LineNumber);

            if (laterAnnotation == null) {
                // we are at the last one (there should be at least one because of CanGoToNextSearchAnnotation)
                laterAnnotation = Annotations[0];
            }
            GotoEntry(laterAnnotation.AssociatedEntry);
        }

        private void OnGoToPreviousSearchAnnotation() {

            if (SelectedLogEntry == null) {
                return;
            }

            // as annotations are sorted by time (through the StartPercent property), the last one is what we need
            AnnotationInfo earlierAnnotation = Annotations.LastOrDefault(ai => ai.IsSearchAnnotation && ai.AssociatedEntry.LineNumber < SelectedLogEntry.LineNumber);

            if (earlierAnnotation == null) {
                // we are at the first one
                earlierAnnotation = Annotations[Annotations.Count - 1];
            }
            GotoEntry(earlierAnnotation.AssociatedEntry);
        }

        private void DeleteSearchBookmarks() {

            IEnumerable<AnnotationInfo> searchAnnotations = Annotations.Where(ai => ai.IsSearchAnnotation).ToList();
            foreach (var ai in searchAnnotations) {
                
                DoDeleteAnnotation(ai, false);
            }
        }

        //private void SearchForMessage(int searchDirection) {
        //    if (_filteredLogEntries.Count == 0) {
        //        Status = "No log entry to search";
        //        return;
        //    }

        //    SetupMessageMatch();

        //    var currentSearchIndex = SelectedLogEntry == null ? 0 : SelectedLogEntryIndex;

        //    // BUG in 1.11: if user filters out the selected log entry, SelectedLogEntryIndex is set to -1 by the grid =>
        //    // lastSearchIndex is also -1 => if the search string is not found, we will wrap indefinitely
        //    int lastSearchIndex = Math.Max(0, currentSearchIndex);

        //    bool hasWrapped = false;
        //    bool hasGoneBackToInitialIndex = false;
        //    bool hasMatched = false;

        //    do {
        //        currentSearchIndex += searchDirection;

        //        if (currentSearchIndex >= _filteredLogEntries.Count) {
        //            currentSearchIndex = 0;
        //            hasWrapped = true;
        //        } else if (currentSearchIndex < 0) {
        //            currentSearchIndex = _filteredLogEntries.Count - 1;
        //            hasWrapped = true;
        //        }

        //        if (IsMessageMatch(currentSearchIndex)) {
        //            GotoEntry(_filteredLogEntries[currentSearchIndex]);
        //            hasMatched = true;
        //        }

        //        // catch the case where we wrapped to the same initial entry
        //        if (hasWrapped && currentSearchIndex == lastSearchIndex) {
        //            hasGoneBackToInitialIndex = true;
        //        }
        //    } while (! hasMatched && ! hasGoneBackToInitialIndex);

        //    if (hasMatched) {

        //        if (hasWrapped) {
        //            Status = "Found text but wrapped around the end of the logs list";
        //        } else {
        //            Status = "Found text";
        //        }
        //    } else {

        //        Status = "The following specified text was not found: " + MessageSearch;
        //    }
        //}

        //private void OnSearchBackwardsForMessage() {
        //    SearchForMessage(-1);
        //}

        private string _messageSearchLowercase;
        private void SetupMessageMatch() {
            _messageSearchLowercase = MessageSearch.Trim().ToLower(CultureInfo.CurrentCulture);
        }
        //private bool IsMessageMatch(int index) {
        //    return _filteredLogEntries[index].LowerCaseMessage.Contains(_messageSearchLowercase);
        //}
        private bool IsMessageMatch(LogEntry le) {
            return le.LowerCaseMessage.Contains(_messageSearchLowercase);
        }

        private void OnDisplayTimeStartPopup() {
            IsShowingTimeStartPopup = true;
        }

        private void OnDisplayShowColumnsPopup() {
            IsShowingShowColumnsPopup = true;
        }

        private void OnDisplayOthersPopup() {
            IsShowingOthersPopup = true;
        }

        private ObservableCollection<double> _originalToolbarColumnWidths;
        // 3 is Time Interval (full), 6 is Show Columns (full), 13 is Others (full)
        private readonly int[] _hidableColumns = { 6, 3, 13 };
        // 2 is Time Start (small), 5 is Show Columns (small), 12 is Others (small)
        // this array could be replaced by _hidableColumns[x] - 1
        private readonly int[] _replacementColumns = { 5, 2, 12};

        private double _neededWidth;

        private void RememberToolbarWidths() {
            _originalToolbarColumnWidths = new ObservableCollection<double>(_toolbarColumnWidths);

            // compute needed widths to show all interesting columns, i.e. all columns minus the replacement columns
            _neededWidth = _originalToolbarColumnWidths.Where( (val, index) => _replacementColumns.All(d => d != index)).Sum();
        }

        private void RecomputeToolbarWidths() {
            
            if (! _isToolbarColumnWidthReady) {
                return; // not ready yet
            }

            // some hidable columns will be shown, some replacement columns will be hidden
            // more precisely, all columns whose index are in _hidableColumns[0.. hideUntil - 1] are hidden and columns whose index are in _hidableColumns[ hideUntil .. max] are shown
            // conversely, the reverse is true for the indices in _replacementColumns: columns whose index are in _replacementColumns[0..hideUntil - 1] are shown and columns whose index are in _replacementColumns[hideUntil.. max] are hidden

            int hideUntil = 0; // this value means that all hidable columns are shown (and thus all replacement columns are hidden)
            // the room that displayed columns neeed
            double displayedWidth = _neededWidth;
            while (displayedWidth > _timelineWidth) {
                
                // we have to hide _hidableColumns [ hideUntil] => compute the new width
                displayedWidth -= _originalToolbarColumnWidths[_hidableColumns[hideUntil]];
                displayedWidth += _originalToolbarColumnWidths[_replacementColumns[hideUntil]];
                hideUntil++;
                if (hideUntil == _hidableColumns.Length) {
                    // cannot continue, no more column to hide
                    break;
                }
            }

            // now show/hide columns
            for (int index = 0; index < hideUntil; index++) {
                _toolbarColumnWidths[_hidableColumns[index]] = 0; // hide
                _toolbarColumnWidths[_replacementColumns[index]] =
                        _originalToolbarColumnWidths[_replacementColumns[index]]; // show again
            }
            for (int index = hideUntil; index < _hidableColumns.Length; index++) {
                _toolbarColumnWidths[_hidableColumns[index]] = _originalToolbarColumnWidths[_hidableColumns[index]];
                _toolbarColumnWidths[_replacementColumns[index]] = 0;
            }
        }

        private void OnManageColors() {

            // make a copy in case the user cancels
            ObservableCollection<ColorRule> tempRules = new ObservableCollection<ColorRule>();
            foreach (ColorRule cr in App.GlobalSettings.ColorRules) {

                tempRules.Add(cr.Clone());
            }

            IUIVisualizer viz = ServiceRegistry.Resolve<IUIVisualizer>();

            ManageColorRulesViewModel mcrvm = new ManageColorRulesViewModel(tempRules);
            mcrvm.SetProcessesList(ProcessColumn.FiltersList);
            mcrvm.SetAreasList(AreaColumn.FiltersList);
            mcrvm.SetCategoriesList(CategoryColumn.FiltersList);
            mcrvm.SetLevelsList(LevelColumn.FiltersList);
            mcrvm.SetEventIDsList(EventColumn.FiltersList);
            mcrvm.DefaultBackgroundColor = App.GlobalSettings.DefaultBackgroundColor;
            mcrvm.DefaultForegroundColor = App.GlobalSettings.DefaultForegroundColor;
            mcrvm.HighlightBackgroundColor = App.GlobalSettings.HighlightBackgroundColor;
            mcrvm.HighlightForegroundColor = App.GlobalSettings.HighlightForegroundColor;

            if (viz.ShowDialog(App.ColorRulesManager, mcrvm) == true) {

                App.GlobalSettings.ColorRules.Clear();
                App.GlobalSettings.ColorRules.AddRange(mcrvm.Rules.Select(r => r.Rule));
                App.GlobalSettings.DefaultBackgroundColor = mcrvm.DefaultBackgroundColor;
                App.GlobalSettings.DefaultForegroundColor = mcrvm.DefaultForegroundColor;
                App.GlobalSettings.HighlightBackgroundColor = mcrvm.HighlightBackgroundColor;
                App.GlobalSettings.HighlightForegroundColor = mcrvm.HighlightForegroundColor;

                RecomputeColorRules();
            }
        }

        private SolidColorBrush _defaultBackgroundBrush, _defaultForegroundBrush;
        private readonly List<ColorCoder> _colorCoders = new List<ColorCoder>();

        private void RecomputeColorRules() {

            SetupColorInfrastructure();
            RecolorLogEntries();
        }

        private void SetupColorInfrastructure() {
        
            _defaultBackgroundBrush = new SolidColorBrush(App.GlobalSettings.DefaultBackgroundColor);
            _defaultForegroundBrush = new SolidColorBrush(App.GlobalSettings.DefaultForegroundColor);
            _defaultBackgroundBrush.Freeze();
            _defaultForegroundBrush.Freeze();

            _colorCoders.Clear();
            _colorCoders.AddRange(App.GlobalSettings.ColorRules.Select(cr => new ColorCoder(cr)));
        }

        private void ComputeColorCode(LogEntry le, bool forceComputation) {

            if (le.AreBrushesSet && !forceComputation) {
                return; // this was already done
            }

            foreach (ColorCoder cr in _colorCoders) {
                
                if (cr.SetColors(le)) {

                    // the rule matched
                    if (! cr.IsBackgroundSet) {
                        le.BackgroundBrush = _defaultBackgroundBrush;
                    }
                    if (! cr.IsForegroundSet) {
                        le.ForegroundBrush = _defaultForegroundBrush;
                    }
                    le.AreBrushesSet = true;
                    return;
                }
            }

            // no rule matched
            le.ForegroundBrush = _defaultForegroundBrush;
            le.BackgroundBrush = _defaultBackgroundBrush;
            le.AreBrushesSet = true;
        }

        public void AddHighlightRule(ColorRule cr) {
            cr.IsSystemDefined = true;
            cr.ForegroundColor = App.GlobalSettings.HighlightForegroundColor;
            cr.BackgroundColor = App.GlobalSettings.HighlightBackgroundColor;

            var rules = App.GlobalSettings.ColorRules;

            // remove existing highlight rule, if any
            var existingRule = rules.FirstOrDefault(r => r.IsSystemDefined && r.Name.StartsWith(HighlightRulePrefix));
            if (existingRule != null) {
                rules.Remove(existingRule);
            }

            rules.Insert(0, cr);

            RecomputeColorRules();
        }

        private void RecolorLogEntries() {

            foreach (LogEntry le in _filteredLogEntries) {
            
                ComputeColorCode(le, true);
            }
        }

        private bool _isMouseButtonDown;
        private bool _isDraggingMouse;
        private double _startingDragX;
        private double _stoppedDragX;
        private DateTime _startingDragTime, _stoppedDragTime;

        private string _timelineSelectionExplanation;
        public string TimelineSelectionExplanation {
            get { return _timelineSelectionExplanation; }
            set {
                if (_timelineSelectionExplanation != value) {
                    _timelineSelectionExplanation = value;
                    OnPropertyChanged("TimelineSelectionExplanation");
                }
            }
        }

        private double _dragRectangleLeft;
        public double DragRectangleLeft {
            get { return _dragRectangleLeft; }
            set {
                if (Math.Abs(_dragRectangleLeft - value) > DoubleTolerance) {
                    _dragRectangleLeft = value;
                    OnPropertyChanged("DragRectangleLeft");
                }
            }
        }

        private double _dragRectangleWidth;
        public double DragRectangleWidth {
            get { return _dragRectangleWidth; }
            set {
                if (Math.Abs(_dragRectangleWidth - value) > DoubleTolerance) {
                    _dragRectangleWidth = value;
                    OnPropertyChanged("DragRectangleWidth");
                }
            }
        }

        private bool CanFilterByTimeline() {
            //HACK: we know that the string is empty when there is no selection
            return !String.IsNullOrEmpty(TimelineSelectionExplanation);
        }

        private void OnFilterByTimeline() {

            if (_startingDragTime < _stoppedDragTime) {
                MinChosenLogDate = _startingDragTime;
                MaxChosenLogDate = _stoppedDragTime;
            } else {
                MinChosenLogDate = _stoppedDragTime;
                MaxChosenLogDate = _startingDragTime;
            }
            Status = "Showing only entries " + TimelineSelectionExplanation;
            ResetDragData();
            FilterAllEntries();
            _isFilteredByTime = true;
        }

        private DateTime _middleLogTime;
        private int _quantityLogTime;
        private bool _isTimeIntervalOK;
        private bool _isFilteredByTime;
        private string _parsedMessageSelectedText;
        private bool _isShowingDropDialog;
        private int _filesDropCount;

        private void OnApplyTimeInterval() {
            
            if (_isTimeIntervalOK) {

                TimeSpan aroundTime;
                switch (AroundTimeUnit) {
                    case SecondUnit:
                        aroundTime = TimeSpan.FromSeconds(_quantityLogTime);
                        break;

                    case HourUnit:
                        aroundTime = TimeSpan.FromHours(_quantityLogTime);
                        break;

                    case MinuteUnit:
                        aroundTime = TimeSpan.FromMinutes(_quantityLogTime);
                        break;

                    default:
                        throw new InvalidOperationException("Unknown time unit");
                }

                MinChosenLogDate = _middleLogTime - aroundTime;
                MaxChosenLogDate = _middleLogTime + aroundTime;
                FilterAllEntries();
                _isFilteredByTime = true;
                ShowTime(_middleLogTime);
                Status = "Showing entries from " + MinChosenLogDate.ToDisplayString() + " to " +
                         MaxChosenLogDate.ToDisplayString();
            }
        }

        private bool CanApplyTimeInterval() {

            if (DateTime.TryParse(ChosenMiddleLogTime, out _middleLogTime)) {

                if (Int32.TryParse(AroundTimeQuantity, out _quantityLogTime)) {

                    _isTimeIntervalOK = true;
                    return true;
                }
            }

            _isTimeIntervalOK = false;
            return false;
        }

        private void OnShowFullTimeInterval() {

            MinChosenLogDate = _minAvailableDate;
            MaxChosenLogDate = _maxAvailableDate;
            FilterAllEntries();
            _isFilteredByTime = false;
            Status = "Showing all entries";
        }

        private bool CanShowFullTimeInterval() {

            return _isFilteredByTime;
        }

        public void StartDrag(double x) {

            _isMouseButtonDown = true;
            _startingDragX = x;
            _stoppedDragX = x;
            ResetDragData();

            DebugLog("Starting drag at {0}", x);
        }

        private void ResetDragData() {
            DragRectangleLeft = _startingDragX;
            DragRectangleWidth = 0.0;
            TimelineSelectionExplanation = String.Empty;
        }

        public void Drag(double x) {

            if (_isMouseButtonDown) {

                _isDraggingMouse = true;
                DebugLog("Dragging for index={0} (from {1})", x, _stoppedDragX);

                double offset = x - _startingDragX;

                if (offset < 0) {

                    // user is moving to the left
                    // => move left coordinate of rectangle
                    DragRectangleLeft = x;
                    offset *= -1; // reverse sign
                } else {

                    // user is moving to the right from the starting point
                    DebugLog("User is dragging to the right");
                }
                DragRectangleWidth = offset /* * IndividualRectangleWidth*/;

                _stoppedDragX = x;
            }
        }

        public void StopDrag(double x) {

            if (_isDraggingMouse) {

                //App.DebugLog("OnMouseUp while dragging for LE={0}", DisplayedDay);

                _stoppedDragX = x;
                _isDraggingMouse = false;

                DateTime firstLogTime = (_minChosenDate == LogReader.MinDate) ? _minAvailableDate : _minChosenDate;
                _startingDragTime = firstLogTime.AddMilliseconds(_startingDragX) ;
                string aEntry = _startingDragTime.ToDisplayString();
                _stoppedDragTime = firstLogTime.AddMilliseconds(_stoppedDragX);
                string bEntry = _stoppedDragTime.ToDisplayString();

                if (_startingDragX < _stoppedDragX) {

                    TimelineSelectionExplanation = "from " + aEntry + " to " + bEntry;
                } else {

                    TimelineSelectionExplanation = "from " + bEntry + " to " + aEntry;
                }
            } else {
                DateTime clickTime = _minChosenDate.AddMilliseconds(_startingDragX) ;
                ShowTime(clickTime);
            }

            _isMouseButtonDown = false;
        }

        private bool CanSearchSelectedMessageText() {

            return !String.IsNullOrEmpty(ParsedMessageSelectedText);
        }

        private void OnSearchSelectedMessageText() {

            Process.Start("http://google.com/search?q=" + ParsedMessageSelectedText);
        }

        private bool CanCopySelectedMessageText() {
            return !String.IsNullOrEmpty(ParsedMessageSelectedText);
        }

        private void OnCopySelectedMessageText() {

            Clipboard.SetText(ParsedMessageSelectedText);
        }

        private void OnIncludeCurrentCorrelation() {

            CorrelationFilter = SelectedLogEntry.Correlation;
            CorrelationFilterCommand.Execute(null);
        }

        private void OnCopyCurrentDate() {
            Clipboard.SetText(SelectedLogEntry.DisplayedDay);
        }

        private void OnCopyCurrentThreadId() {
            Clipboard.SetText(SelectedLogEntry.Tid);
        }

        private void OnCopyCurrentMessage() {
            Clipboard.SetText(SelectedLogEntry.Message);
        }

        private void OnCopyCurrentCorrelation() {
            Clipboard.SetText(SelectedLogEntry.Correlation);
        }

        private void OnHighlightCurrentCorrelation() {
            ColorRule cr = new ColorRule {
                Correlation = SelectedLogEntry.Correlation,
                Name = "high" + SelectedLogEntry.Correlation,
            };
            AddHighlightRule(cr);
        }

        private void OnExcludeEntriesBefore() {

            if (SelectedLogEntry == null) {
                return;
            }
            MinChosenLogDate = SelectedLogEntry.Day;
            Status = "Showing only entries starting from " + SelectedLogEntry.DisplayedDay;
            FilterAllEntries();
            _isFilteredByTime = true;
        }
        private void OnExcludeEntriesAfter() {

            if (SelectedLogEntry == null) {
                return;
            }
            MaxChosenLogDate = SelectedLogEntry.Day;
            Status = "Showing only entries up to " + SelectedLogEntry.DisplayedDay;
            FilterAllEntries();
            _isFilteredByTime = true;
        }

        private void OnUpdateAnnotation() {

            if (SelectedLogEntry == null) {
                return;
            }

            var selectedAnnotation = SelectedLogEntry.Annotation;

            DoUpdateAnnotation(selectedAnnotation);
        }

        private void OnUpdateAnnotationExternal(object parameter) {

            LogEntry le = parameter as LogEntry;

            if (le == null) {
                return;
            }
            DoUpdateAnnotation(le.Annotation);
        }

        private void DoUpdateAnnotation(AnnotationInfo annotation) {

            IMessageToUser mu = ServiceRegistry.Resolve<IMessageToUser>();

            string newAnnotation = mu.Prompt(Title, "Enter annotation:", annotation?.Annotation);

            if (!String.IsNullOrEmpty(newAnnotation)) {
                if (annotation != null) {
                    annotation.Annotation = newAnnotation;
                    SerializeAnnotations(annotation);
                } else {
                    AnnotationInfo ai = new AnnotationInfo {
                        Annotation = newAnnotation,
                        HeightPercent = 1, // not very useful
                        Brush = s_annotationBrush,
                        //LightColor = s_annotationLightColor,
                        //DarkColor =  s_annotationDarkColor,
                        StartPercent = ComputePercentAndMoveIfNeeded(SelectedLogEntry),
                        IsVisible = true,
                        IsPersistent = true,
                    };
                    SelectedLogEntry.Annotation = ai;
                    ai.AssociatedEntry = SelectedLogEntry;

                    AddAnnotation(ai, true);
                }
            }
        }

        // to be called before adding an annotation in the annotations list
        private int ComputePercentAndMoveIfNeeded(LogEntry logEntry) {

            int result = ComputePercent(logEntry);

            while (result < 99 && Annotations.Any(ai => ai.StartPercent == result)) {
                result ++;
            }

            return result;
        }

        private int ComputePercent(LogEntry logEntry) {

            return 100 * logEntry.LineNumber / _filteredLogEntries.Count;
        }

        private bool CanDeleteAnnotation() {

            return SelectedLogEntry?.Annotation != null;
        }

        private void OnDeleteAnnotation() {

            var selectedAnnotation = SelectedLogEntry?.Annotation;
            if (selectedAnnotation != null) {

                DoDeleteAnnotation(selectedAnnotation, true);
            }
        }

        private void OnDeleteAnnotationExternal(object parameter) {

            LogEntry le = parameter as LogEntry;

            var selectedAnnotation = le?.Annotation;

            if (selectedAnnotation != null) {

                DoDeleteAnnotation(selectedAnnotation, true);
            }
        }

        private void AddAnnotation(AnnotationInfo annotation, bool toBeSerialized) {
            
            Annotations.Add(annotation);
            ComputeColorCode(annotation.AssociatedEntry, forceComputation:true);

            if (toBeSerialized && annotation.IsPersistent) {
                SerializeAnnotations(annotation);
            }
        }

        private void DoDeleteAnnotation(AnnotationInfo annotation, bool toBeSerialized) {

            Annotations.Remove(annotation);
            ComputeColorCode(annotation.AssociatedEntry, forceComputation:true);

            if (toBeSerialized && annotation.IsPersistent) {
                SerializeAnnotations(annotation);
            }
        }

        /// <summary>
        /// Writes a file containing all annotations coming from the same log file as the given parameter
        /// </summary>
        /// <param name="annotation">the involved annotation</param>
        private void SerializeAnnotations(AnnotationInfo annotation) {

            LogFileInfo lfi = FindLogFileContaining(annotation);
            if (lfi == null) {
                return; // weird
            }

            IEnumerable<AnnotationInfo> annotationsFromSameFile = Annotations.Where(ann => ann.IsPersistent && lfi == FindLogFileContaining(ann));
            WriteAnnotations(lfi, annotationsFromSameFile);
        }

        /// <summary>
        /// returns true if we could find a deserialized annotation (we may be called too early, while there not any log entry in memory)
        /// </summary>
        /// <param name="lfi"></param>
        /// <returns></returns>
        private bool ReadAnnotations(LogFileInfo lfi) {

            string fileName = ComputeAnnotationFileName(lfi);

            try {
                IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForDomain();

                if (isf.GetFileNames(fileName).Length == 0) {
                    // if there is no file, tell our caller that there is no need to retry
                    return true;
                }

                using (
                    IsolatedStorageFileStream annotationsStream = new IsolatedStorageFileStream(fileName,
                        FileMode.OpenOrCreate, FileAccess.Read, isf)) {

                    XmlSerializer ser = new XmlSerializer(typeof(List<AnnotationInfoSerialized>));
                    List<AnnotationInfoSerialized> annotationsFromFile = (List<AnnotationInfoSerialized>) ser.Deserialize(annotationsStream);

                    return AddSerializedAnnotations(annotationsFromFile);
                }
            } catch (Exception ex) {

                App.DebugLog("Error while loading annotations for " + fileName + ": " + ex);
                // no need to retry
                return true;
            }
        }

        /// <summary>
        /// Return true if at least one annotation was mapped to a log entry
        /// </summary>
        /// <param name="annotations"></param>
        /// <returns></returns>
        private bool AddSerializedAnnotations(IEnumerable<AnnotationInfoSerialized> annotations) {

            bool result = false; // pessimistic

            foreach (var annotation in annotations) {

                bool isVisible;
                LogEntry le = FindLogEntry(annotation, out isVisible);

                if (le != null) {
                    AnnotationInfo ai = annotation.ToAnnotationInfo();
                    ai.AssociatedEntry = le;
                    ai.Brush = s_annotationBrush;
                    //ai.LightColor = s_annotationLightColor;
                    //ai.DarkColor = s_annotationDarkColor;
                    ai.IsVisible = isVisible;
                    // it is necessary to compute the position right now in order to add the annotation at the right position in the list
                    ai.StartPercent = ComputePercent(ai.AssociatedEntry);
                    ai.IsPersistent = true;

                    le.Annotation = ai;
                    AddAnnotation(ai, false);

                    result = true;
                }
            }

            return result;
        }

        private LogEntry FindLogEntry(AnnotationInfoSerialized annotation, out bool isVisible) {

            foreach (var le in _allLogEntries) {

                if (le.Day == annotation.DateTime && le.Message == annotation.LogText) {

                    isVisible = _filteredLogEntries.Contains(le);
                    return le;
                }
            }

            isVisible = false;
            return null;
        }

        private void WriteAnnotations(LogFileInfo lfi, IEnumerable<AnnotationInfo> annotationsFromSameFile) {

            string fileName = ComputeAnnotationFileName(lfi);

            try {
                using (
                    IsolatedStorageFileStream annotationsStream = new IsolatedStorageFileStream(fileName, FileMode.Create, FileAccess.Write)) {

                    XmlSerializer ser = new XmlSerializer(typeof(List<AnnotationInfoSerialized>));
                    ser.Serialize(annotationsStream, annotationsFromSameFile.Select(ai => new AnnotationInfoSerialized(ai)).ToList());
                }
            } catch (Exception ex) {
                App.DebugLog("Error while saving annotations for file " + fileName + ": " + ex);
            }


        }

        private string ComputeAnnotationFileName(LogFileInfo lfi) {
            return lfi.OriginalFile.Name + ".xml";
        }

        private LogFileInfo FindLogFileContaining(AnnotationInfo annotation) {

            LogEntry le = annotation.AssociatedEntry;
            DateTime date = le.Day;

            for (int index = 1; index < _filesInMemory.Count; index++) {

                if (_filesInMemory[index].DateTime > date) {
                    return _filesInMemory[index - 1];
                }
            }

            return _filesInMemory[0];
        }

        private void DeserializeAnnotations() {

            foreach (var lfi in _filesInMemory) {

                if (!lfi.AreAnnotationsDeserialized) {

                    lfi.AreAnnotationsDeserialized = ReadAnnotations(lfi);
                }
            }
        }

        private void UpdateAnnotationPositions() {
            foreach (AnnotationInfo ai in Annotations) {
                ai.StartPercent = ComputePercent(ai.AssociatedEntry);
            }

            // modify the StartPercent attribute if it conflicts with the previous one
            foreach (AnnotationInfo ai in Annotations) {

                while (Annotations.Any(info => info.StartPercent == ai.StartPercent && info != ai)) {
                    ai.StartPercent++;
                }
            }
        }

        private void UpdateAnnotationVisibility(LogEntry logEntry, bool isVisible) {
            if (logEntry.Annotation != null) {
                logEntry.Annotation.IsVisible = isVisible;
            }
        }
    }

    /// <summary>
    /// ViewModel -> View communication to get the list of selected items in the main list view
    /// </summary>
    public interface IGetSelection {

        IList SelectedItems { get; }
    }

    /// <summary>
    /// Given to LogColumnViewModel to call back here
    /// </summary>
    public interface IColumnCommunication {

        void FilterAllEntries();
        LogEntry SelectedLogEntry { get; }
        string HighlightRulePrefix { get; }
        void AddHighlightRule(ColorRule cr);
    }
}
